UIScrollView – Adjust User Scroll Amount Akin to Scrubbing? - ios

I have UIScrollview that the user scrolls up and down.
Is there a way to adjust how much the user's drag of the finger results in the final scroll amount?
I was looking at UIScrollview delegate methods, but haven't found a place to alter that.
scrollViewDidScroll(_:) seems too late since this is AFTER the event.

iOS users are very familiar with using scrolling views, so changing the "scroll speed" may be confusing. However, it's your app :)
Give this a try...
When the user Begins dragging, we'll grab the .contentOffset.y as a "starting point." In scrollViewDidScroll, we'll get the difference between the new .contentOffset.y and the startingY ... multiply that by the speed factor ... and then change the .contentOffset.y.
Note that manually setting .contentOffset.y triggers scrollViewDidScroll, so we'll also need to set a bool flag to prevent recursion:
class SlowScrollVC: UIViewController, UIScrollViewDelegate {
// scrollSpeed --- example values
// 1.0 == normal
// 1.5 == fast
// 0.5 == slow
var scrollSpeed: CGFloat = 0.5
var startingOffsetY: CGFloat = 0
var bManualOffset: Bool = false
let scrollView = UIScrollView()
override func viewDidLoad() {
super.viewDidLoad()
// add a bunch of labels and buttons so we have something to scroll
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 40
for i in 1...20 {
let v = UILabel()
v.text = "Label \(i)"
v.textAlignment = .center
v.backgroundColor = .cyan
stack.addArrangedSubview(v)
let b = UIButton()
b.setTitle("Button \(i)", for: [])
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
b.backgroundColor = .systemBlue
b.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
stack.addArrangedSubview(b)
}
stack.translatesAutoresizingMaskIntoConstraints = false
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(stack)
view.addSubview(scrollView)
// so we can see the scroll view frame
scrollView.backgroundColor = .yellow
let g = view.safeAreaLayoutGuide
let cg = scrollView.contentLayoutGuide
let fg = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
stack.topAnchor.constraint(equalTo: cg.topAnchor, constant: 8.0),
stack.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 8.0),
stack.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -8.0),
stack.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: -8.0),
stack.widthAnchor.constraint(equalTo: fg.widthAnchor, constant: -16.0),
])
scrollView.delegate = self
// you may also want to adjust .decelerationRate
// try various values to see the result
//scrollView.decelerationRate = UIScrollView.DecelerationRate(rawValue: 0.99)
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
startingOffsetY = scrollView.contentOffset.y
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !bManualOffset {
// get the difference between previous offset.y and new offset.y
let diff = scrollView.contentOffset.y - startingOffsetY
// adjust by scroll-speed factor
let newY = startingOffsetY + diff * scrollSpeed
// prevent recursion
bManualOffset = true
// set adjusted offset.y
scrollView.contentOffset.y = newY
// update start Y
startingOffsetY = newY
}
bManualOffset = false
}
#objc func btnTap(_ sender: UIButton) {
// just to confirm we tapped a button
print("Tap:", sender.currentTitle)
}
}

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:

Swift - Update auto layout properties of UI components programmatically

To design and create my UI, I always use auto layouts and do it programmatically instead of using storyboard.
In every view class of mine, I have a method called
private func setupView(frame:CGRect) {
/* START CONTAINER VIEW */
containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false
addSubview(containerView)
containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: frame.width * (13 / IPHONE8_SCREEN_WIDTH)).isActive = true
containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -frame.width * (13 / IPHONE8_SCREEN_WIDTH)).isActive = true
containerView.topAnchor.constraint(equalTo: topAnchor, constant: frame.height * (26 / IPHONE8_SCREEN_HEIGHT)).isActive = true
containerView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
containerView.backgroundColor = UIColor.white
/* END CONTAINER VIEW */
...
}
to initialize the components. Now let's say, in the method above, I initialize 10 UI components which are properly displayed when I run my code. However, depending on some variables, I have another function that is being called
private func addNextRoundInformation() {
..
nextRoundLabel = UILabel()
nextRoundLabel.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(nextRoundLabel)
nextRoundLabel.leadingAnchor.constraint(equalTo: currentRoundLabel.leadingAnchor).isActive = true
nextRoundLabel.widthAnchor.constraint(equalTo:currentRoundLabel.widthAnchor).isActive = true
nextRoundLabel.topAnchor.constraint(equalTo: roundEndsInLabel.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT)).isActive = true
}
which should place a new label between some others which were already initialized.
Of course, when putting the new label between some particular ones, I also update the auto layout constraints of the of the bottom label like
private func updateNumberOfWinnersLabelConstraint() {
numberOfWinnersPerRoundLabel.topAnchor.constraint(equalTo: nextRoundLabel.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT)).isActive = true
numberOfWinnersPerRoundLabelValue.topAnchor.constraint(equalTo: nextRoundLabelValue.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT)).isActive = true
}
The topAnchor of each label depends on the bottom anchor of the previous one.
With this approach, I can't see nextRoundLabel at all. It only appears, if I initialize it in the private func setupView(frame:CGRect) {}
Why?
You could do this with "Top-to-Bottom" constraints, but it would be rather complex.
You would need to essentially create a "Linked List" to track each view, the views above and below it, and its constraints.
So, to "insert" a new view after the 3rd view, you would need to:
deactivate the inter-view constraints
insert the new view into the linked list
re-create and activate the new constraints
Putting your views in a UIStackView turns that process into a single line of code:
stackView.insertArrangedSubview(newView, at: 3)
Here's a quick example:
class ViewController: UIViewController {
let testView: SampleView = SampleView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
let infoLabel: UILabel = {
let v = UILabel()
v.textAlignment = .center
v.numberOfLines = 0
v.text = "Tap to Insert \"New Label\"\nafter \"Label 3\""
return v
}()
let btn: UIButton = {
let v = UIButton()
v.setTitle("Insert", for: [])
v.setTitleColor(.white, for: .normal)
v.setTitleColor(.lightGray, for: .highlighted)
v.backgroundColor = .systemBlue
return v
}()
[infoLabel, btn, testView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
infoLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
infoLabel.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.75),
infoLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
btn.topAnchor.constraint(equalTo: infoLabel.bottomAnchor, constant: 20.0),
btn.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.75),
btn.centerXAnchor.constraint(equalTo: g.centerXAnchor),
testView.topAnchor.constraint(equalTo: btn.bottomAnchor, constant: 40.0),
testView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
testView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
])
btn.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
}
#objc func btnTapped(_ sender: Any?) {
testView.insertNew()
}
}
class SampleView: UIView {
var containerView: UIView!
var stackView: UIStackView!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
backgroundColor = .red
setupView(frame: .zero)
}
private func setupView(frame:CGRect) {
/* START CONTAINER VIEW */
containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false
addSubview(containerView)
stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = 8
stackView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(stackView)
NSLayoutConstraint.activate([
containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
containerView.topAnchor.constraint(equalTo: topAnchor, constant: 12),
containerView.bottomAnchor.constraint(equalTo: bottomAnchor),
stackView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 8.0),
stackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8.0),
stackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8.0),
])
containerView.backgroundColor = UIColor.white
/* END CONTAINER VIEW */
// add 10 labels to the stack view
for i in 1...10 {
let v = UILabel()
v.text = "Label \(i)"
v.backgroundColor = .green
stackView.addArrangedSubview(v)
}
}
func insertNew() {
let v = UILabel()
v.text = "New Label"
v.backgroundColor = .cyan
v.translatesAutoresizingMaskIntoConstraints = false
// we're adding a label after 3rd label
stackView.insertArrangedSubview(v, at: 3)
}
}
It starts looking like this:
after tapping the "Insert" button, it looks like this:
Edit
To explain why your current approach isn't working...
Starting with this layout:
each label's Top is constrained to the previous label's Bottom (with constant: spacing).
Those constraints are indicated by the blue arrows.
You then want to "insert" Next Round Label between Round Ends In and Winners Per Round:
Your code:
adds the label
adds a constraint from the Top of Next Round Label to the Bottom of Round Ends In
adds a constraint from the Top of Winners Per Round to the Bottom of Next Round Label
but... Winners Per Round already has a .topAnchor connected to Round Ends In, so it now has two top anchors.
The conflicting constraints are shown in red:
As I said, I think your description of what you're trying to do would lend itself to using stack views, and would make "inserting" views so much easier.
But, if you need to stick with your current "Top-to-Bottom" constraints approach, you have several options.
One - remove all the labels, then re-add and re-constrain them, including the one you're inserting.
Two - track the constraints (using an array or custom object properties) so you can deactivate the conflicting constraint(s).
Three - use some code along the lines of
let theConstraint = containerView.constraints.first(where: {($0.secondItem as? UILabel) == roundEndsInLabel})
to "find" the constraint that needs to be deactivated.
I found a working solution:
First, I declared two helper variables
/* START HELPER VARIABLES */
var numberOfWinnersLabelTopConstraint:NSLayoutConstraint?
var numberOfWinnersLabelValueTopConstraint:NSLayoutConstraint?
/* END HELPER VARIABLES */
then I made some minor adjustments in my setupView function:
private func setupView(frame:CGRect) {
...
/* START NUMBER OF WINNERS PER ROUND LABEL */
numberOfWinnersPerRoundLabel = UILabel()
numberOfWinnersPerRoundLabel.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(numberOfWinnersPerRoundLabel)
numberOfWinnersPerRoundLabel.leadingAnchor.constraint(equalTo: currentRoundLabel.leadingAnchor).isActive = true
numberOfWinnersPerRoundLabel.widthAnchor.constraint(equalTo:currentRoundLabel.widthAnchor).isActive = true
// numberOfWinnersPerRoundLabel.topAnchor.constraint(equalTo: roundEndsInLabel.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT)).isActive = true // Replacing with helper variable
numberOfWinnersLabelTopConstraint = numberOfWinnersPerRoundLabel.topAnchor.constraint(equalTo: roundEndsInLabel.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT))
numberOfWinnersLabelTopConstraint?.isActive = true
numberOfWinnersPerRoundLabel.text = NSLocalizedString(NUMBER_OF_WINNERS_PER_ROUND_TEXT, comment: "")
numberOfWinnersPerRoundLabel.textColor = .darkGray
numberOfWinnersPerRoundLabel.adjustsFontSizeToFitWidth = true
numberOfWinnersPerRoundLabel.font = .systemFont(ofSize: 12)
/* END NUMBER OF WINNERS PER ROUND LABEL */
/* START NUMBER OF WINNERS PER ROUND LABEL VALUE */
numberOfWinnersPerRoundLabelValue = UILabel()
numberOfWinnersPerRoundLabelValue.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(numberOfWinnersPerRoundLabelValue)
numberOfWinnersPerRoundLabelValue.leadingAnchor.constraint(equalTo: currentRoundLabelValue.leadingAnchor).isActive = true
numberOfWinnersPerRoundLabelValue.widthAnchor.constraint(equalTo:currentRoundLabelValue.widthAnchor).isActive = true
// numberOfWinnersPerRoundLabelValue.topAnchor.constraint(equalTo: numberOfWinnersPerRoundLabel.topAnchor).isActive = true // replacing with helper variable
numberOfWinnersLabelValueTopConstraint = numberOfWinnersPerRoundLabelValue.topAnchor.constraint(equalTo: numberOfWinnersPerRoundLabel.topAnchor)
numberOfWinnersLabelValueTopConstraint?.isActive = true
numberOfWinnersPerRoundLabelValue.textColor = .black
/* END NUMBER OF WINNERS PER ROUND LABEL VALUE */
By introducing the helper variables, I could easily deactivate the topConstraint when adding the nextRoundLabel
private func addNextRoundInformation() {
nextRoundLabel = UILabel()
nextRoundLabel.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(nextRoundLabel)
nextRoundLabel.leadingAnchor.constraint(equalTo: currentRoundLabel.leadingAnchor).isActive = true
nextRoundLabel.widthAnchor.constraint(equalTo:currentRoundLabel.widthAnchor).isActive = true
nextRoundLabel.topAnchor.constraint(equalTo: roundEndsInLabel.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT)).isActive = true
nextRoundLabel.text = "Next round starts in"
nextRoundLabel.textColor = .darkGray
nextRoundLabel.font = .systemFont(ofSize: 12)
nextRoundLabelValue = UILabel()
nextRoundLabelValue.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(nextRoundLabelValue)
nextRoundLabelValue.leadingAnchor.constraint(equalTo: currentRoundLabelValue.leadingAnchor).isActive = true
nextRoundLabelValue.widthAnchor.constraint(equalTo:currentRoundLabelValue.widthAnchor).isActive = true
nextRoundLabelValue.topAnchor.constraint(equalTo:nextRoundLabel.topAnchor).isActive = true
nextRoundLabelValue.textColor = .black
nextRoundLabelValue.text = "Next round label value"
nextRoundLabelValue.font = .systemFont(ofSize: 14)
}
private func updateNumberOfWinnersLabelConstraint() {
numberOfWinnersLabelTopConstraint?.isActive = false // Deactivate previous constraint
numberOfWinnersLabelValueTopConstraint?.isActive = false
numberOfWinnersLabelTopConstraint = numberOfWinnersPerRoundLabel.topAnchor.constraint(equalTo: nextRoundLabel.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT))
numberOfWinnersLabelTopConstraint?.isActive = true
numberOfWinnersLabelValueTopConstraint = numberOfWinnersPerRoundLabelValue.topAnchor.constraint(equalTo: nextRoundLabelValue.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT))
numberOfWinnersLabelValueTopConstraint?.isActive = true
}
Basically, I only had to update the topConstraints of the numberOfWinnersPerRoundLabel and numberOfWinnersPerRoundLabelValue
since everything else would be the same. No changes needed for currentRoundLabel.
I tested it and it worked!

UIScrollView: Lock scrolling to vertical axis when zoomed in without changing the contentView

What’s the best way to dynamically lock scrolling in a UIScrollView to the vertical axis depending on the zoom scale?
I want to allow scrolling a large canvas in any direction when zoomed out (scrollView.zoomScale < 1.0)
but prevent horizontal scrolling completely when zoomed in (scrollView.zoomScale == 1.0).
The challenge here is that UIScrollView doesn’t seem to have a built-in setting to limit scrolling to one direction if the contentView is larger than the viewport in both directions. I would like to use the same large contentView but disallow horizontal scrolling when zoomed in.
(I know about scrollView.isDirectionalLockEnabled, but that’s not what I need: It only checks whether the user’s pan gesture has a dominant scrolling direction and then dynamically locks scrolling to either direction.)
Thanks!
If I understand your goal correctly...
You have a "contentView" that is larger than the scroll view
if the zoom scale is 1.0, only allow vertical scrolling
if the zoom scale is less than 1.0, allow both vertical and horizontal scrolling
So, if we have a scroll view frame size of 388 x 661 and a "contentView" with a size of 2100 x 2100, we start like this at zoom scale 1.0 (the bright-green is the scroll view frame):
and only vertical scrolling is allowed.
If the user zooms-out to, say, 0.8 scale:
both vertical and horizontal scrolling is allowed.
If the user then zooms-in back to 1.0 scale:
we're back to only vertical scrolling.
You can accomplish that by conforming your controller to UIScrollViewDelegate, assign self as the scrollView's delegate, add a "last scrollView content offset X" var, and then implement scrollViewDidScroll():
var lastOffsetX: CGFloat = 0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// if zoom scale is 1.0
// don't allow horizontal scrolling
if scrollView.zoomScale == 1.0 {
scrollView.contentOffset.x = lastOffsetX
return
}
// zoom scale is less than 1.0, so
// allow the scroll and update lastX
lastOffsetX = scrollView.contentOffset.x
}
Here's a complete example you can try out:
class ExampleViewController: UIViewController, UIScrollViewDelegate {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .systemYellow
return v
}()
let contentView: UIView = {
let v = UIView()
v.backgroundColor = .systemTeal
return v
}()
let infoLabel: UILabel = {
let v = UILabel()
return v
}()
// we'll use this to track the current content X offset
var lastOffsetX: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
scrollView.addSubview(contentView)
view.addSubview(scrollView)
view.addSubview(infoLabel)
[contentView, scrollView, infoLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
// let's add a 8 x 8 "grid" of labels to the content view
let outerVerticalStack = UIStackView()
outerVerticalStack.axis = .vertical
outerVerticalStack.spacing = 20
outerVerticalStack.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(outerVerticalStack)
var j: Int = 1
for _ in 1...8 {
let rowStack = UIStackView()
rowStack.axis = .horizontal
rowStack.spacing = 20
rowStack.distribution = .fillEqually
for _ in 1...8 {
let v = UILabel()
v.font = .systemFont(ofSize: 48.0, weight: .regular)
v.text = "\(j)"
v.textAlignment = .center
v.backgroundColor = .green
v.widthAnchor.constraint(equalToConstant: 240.0).isActive = true
v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
rowStack.addArrangedSubview(v)
j += 1
}
outerVerticalStack.addArrangedSubview(rowStack)
}
let safeG = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 20.0),
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -20.0),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -120.0),
contentView.topAnchor.constraint(equalTo: contentG.topAnchor),
contentView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor),
outerVerticalStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20.0),
outerVerticalStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20.0),
outerVerticalStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20.0),
outerVerticalStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20.0),
// put the info label below the scroll view
infoLabel.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 20.0),
infoLabel.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 20.0),
infoLabel.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -20.0),
])
// we'll update min zoom in viewDidAppear
// (after all views have been laid out)
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 1.0
// we need to disable zoom bouncing, or
// we get really bad positioning effect
// when zooming in past 1.0
scrollView.bouncesZoom = false
// assign the delegate
scrollView.delegate = self
// update the info label
updateInfo()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// update min zoom scale so we can only "zoom out" until
// the content view fits the scroll view frame
if scrollView.minimumZoomScale == 1.0 {
print(contentView.frame.size)
let xScale = scrollView.frame.width / contentView.frame.width
let yScale = scrollView.frame.height / contentView.frame.height
scrollView.minimumZoomScale = min(xScale, yScale)
}
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return contentView
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// if zoom scale is 1.0
// don't allow horizontal scrolling
if scrollView.zoomScale == 1.0 && !scrollView.isZooming {
scrollView.contentOffset.x = lastOffsetX
return
}
// zoom scale is less than 1.0, so
// allow the scroll and update lastX
lastOffsetX = scrollView.contentOffset.x
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
updateInfo()
}
func updateInfo() {
let s = String(format: "%0.4f", scrollView.zoomScale)
infoLabel.text = "Zoom Scale: \(s)"
}
}

Using ScrollView with StackView as subview and UIViews as children of StackView

I'm struggling to get my scroll view to work programatically.
I have a view controller that instantiates a UIScrollView with the following constraints
class HomeTabBarController: ViewController {
let homePageView = HomePageView()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
homePageView.setupHomePage()
view.addSubview(homePageView)
///constraints
homePageView.stackView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
homePageView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
homePageView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true;
homePageView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true;
homePageView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -83).isActive = true;
}
}
The HomePageView (UIScrollView) has a UIStackView and instantiates 3 more UIViews which are the UIStackView's children. The code and constraints are as follows
class HomePageView: UIScrollView {
var homePageCarrousel: HomePageCarrousel?
var homePageSocial: HomePageSocialUp?
var homePageAboutUs: HomePageAboutUs?
var stackView = UIStackView()
func setupHomePage() {
translatesAutoresizingMaskIntoConstraints = false
homePageCarrousel = HomePageCarrousel()
homePageSocial = HomePageSocialUp()
homePageAboutUs = HomePageAboutUs()
guard let homePageSocial = homePageSocial, let homePageCarrousel = homePageCarrousel, let homePageAboutUs = homePageAboutUs else { return }
homePageSocial.setupSocialHeader()
stackView.addArrangedSubview(homePageSocial)
homePageCarrousel.setupCarrousel()
stackView.addArrangedSubview(homePageCarrousel)
homePageAboutUs.setup()
stackView.addArrangedSubview(homePageAboutUs)
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 10
setLayout()
}
func setLayout(){
guard let homePageSocial = homePageSocial, let homePageCarrousel = homePageCarrousel, let homePageAboutUs = homePageAboutUs else { return }
///header
homePageSocial.leadingAnchor.constraint(equalTo: stackView.leadingAnchor).isActive = true
homePageSocial.topAnchor.constraint(equalTo: stackView.topAnchor).isActive = true
homePageSocial.trailingAnchor.constraint(equalTo: stackView.trailingAnchor).isActive = true
///carrousel
homePageCarrousel.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 20).isActive = true
homePageCarrousel.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 20).isActive = true
homePageCarrousel.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: -20).isActive = true
///about us
homePageAboutUs.leadingAnchor.constraint(equalTo: stackView.leadingAnchor).isActive = true
homePageAboutUs.topAnchor.constraint(equalTo: homePageCarrousel.bottomAnchor, constant: 10).isActive = true
homePageAboutUs.trailingAnchor.constraint(equalTo: stackView.trailingAnchor).isActive = true
homePageAboutUs.bottomAnchor.constraint(equalTo: stackView.bottomAnchor).isActive = true
///stackview
self.stackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true;
self.stackView.topAnchor.constraint(equalTo: topAnchor).isActive = true;
self.stackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true;
self.stackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true;
}
func dispose() {
homePageSocial = nil
homePageCarrousel = nil
homePageAboutUs = nil
subviews.forEach{$0.removeFromSuperview()}
}
}
Each of the children UIView (homePageSocial, homePageCarrousel and homePageAboutUs) have constraints have also got constraints:
HomePageCarrousel
Loads UIImageView and after adding as subview sets the constraint as
heightAnchor.constraint(equalToConstant: imageView.frame.height).isActive = true
HomePageAboutUs
Has 3 UITextviews (headerText, bodyTextLeft, bodyTextRight)
Header on top, bodytextLeft below it being 50% width of screen x = 0 and bodyTextRight x = width of bodyTextLeft.
Constraints are as follows
heightAnchor.constraint(equalToConstant: headerText.frame.height + bodyTextLeft.frame.height).isActive = true
bodyTextLeft.topAnchor.constraint(equalTo: headerText.bottomAnchor, constant: 15).isActive = true
bodyTextLeft.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width / 2).isActive = true
bodyTextRight.topAnchor.constraint(equalTo: headerText.bottomAnchor, constant: 15).isActive = true
bodyTextRight.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width / 2).isActive = true
bodyTextRight.leadingAnchor.constraint(equalTo: bodyTextLeft.trailingAnchor).isActive = true
HomePageSocialUp
A basic header with an icon and a constraint of
heightAnchor.constraint(equalToConstant: 325).isActive = true
After all that set I still can't get my ui scroll view to scroll on the y axis leaving my text of homePageAboutUs below the tabBar and off screen.
What am I doing wrong here?
Thanks in advance
First - the main purpose of a UIStackView is to arrange its subviews, so it is wrong to add position constraints to those subviews.
Next, when adding subviews to the "root" view of a controller (such as your scroll view), be sure to constrain them to the Safe Area Layout Guide.
Third, constrain the content of your scroll view to its Content Layout Guide.
I'm kind of taking your descriptions and hoping I'm close to what you're going for here:
and after scrolling down:
Here is your code, modified to produce that result:
class HomeTabBarController: UIViewController {
let homePageView = HomePageView()
override func viewDidLoad() {
super.viewDidLoad()
homePageView.setupHomePage()
view.addSubview(homePageView)
// respect safe area
let g = view.safeAreaLayoutGuide
///constraints
NSLayoutConstraint.activate([
homePageView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
homePageView.topAnchor.constraint(equalTo: g.topAnchor),
homePageView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
homePageView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
}
}
class HomePageSocialUp: UIView {
func setupSocialHeader() -> Void {
translatesAutoresizingMaskIntoConstraints = false
backgroundColor = .red
let imgView = UIImageView()
imgView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imgView)
NSLayoutConstraint.activate([
// constrain image view 20-pts on each side
imgView.topAnchor.constraint(equalTo: topAnchor, constant: 20.0),
imgView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
imgView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
imgView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20.0),
// Height = 325
imgView.heightAnchor.constraint(equalToConstant: 325.0),
])
if let img = UIImage(named: "myHeaderImage") {
imgView.image = img
}
}
}
class HomePageCarrousel: UIView {
func setupCarrousel() -> Void {
translatesAutoresizingMaskIntoConstraints = false
backgroundColor = .green
let imgView = UIImageView()
imgView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imgView)
NSLayoutConstraint.activate([
// constrain image view 20-pts on each side
imgView.topAnchor.constraint(equalTo: topAnchor, constant: 20.0),
imgView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
imgView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
imgView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20.0),
// let's make it 3:2 ratio
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor, multiplier: 2.0 / 3.0)
])
if let img = UIImage(named: "myCarouselImage") {
imgView.image = img
}
}
}
class HomePageAboutUs: UIView {
let headerText = UILabel()
let bodyTextLeft = UILabel()
let bodyTextRight = UILabel()
func setup() -> Void {
translatesAutoresizingMaskIntoConstraints = false
backgroundColor = .blue
[headerText, bodyTextLeft, bodyTextRight].forEach {
// keep label height to text content
$0.setContentHuggingPriority(.required, for: .vertical)
$0.setContentCompressionResistancePriority(.required, for: .vertical)
// allow word-wrap
$0.numberOfLines = 0
// yellow background
$0.backgroundColor = .yellow
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
NSLayoutConstraint.activate([
// header text 8-pts from Top / Leading / Trailing
headerText.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
headerText.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
headerText.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
// left text 8-pts from Bottom of header text, 8-pts Leading
bodyTextLeft.topAnchor.constraint(equalTo: headerText.bottomAnchor, constant: 8.0),
bodyTextLeft.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
// right text 8-pts from Bottom of header text, 8-pts Trailing
bodyTextRight.topAnchor.constraint(equalTo: headerText.bottomAnchor, constant: 8.0),
bodyTextRight.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
// 8-pts between left and right text
bodyTextRight.leadingAnchor.constraint(equalTo: bodyTextLeft.trailingAnchor, constant: 8.0),
// left and right text equal width
bodyTextLeft.widthAnchor.constraint(equalTo: bodyTextRight.widthAnchor),
// constrain Bottom of both to <= 8 (at least 8-pts
bodyTextLeft.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8.0),
bodyTextRight.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8.0),
])
headerText.font = .systemFont(ofSize: 20, weight: .regular)
bodyTextLeft.font = .systemFont(ofSize: 16, weight: .regular)
bodyTextRight.font = .systemFont(ofSize: 16, weight: .regular)
// texxt alignment
headerText.textAlignment = .center
bodyTextLeft.textAlignment = .left
bodyTextRight.textAlignment = .right
// some sample text
headerText.text = "This is the text for the About Us Header label. It will, of course, wrap onto multiple lines when needed, and auto-size it's height to fit the text."
bodyTextLeft.text = "Left label with\nembedded newlines\nso we can see it grow\nto fit the text.\nLine 5\nLine 6\nLine 7"
bodyTextRight.text = "Right label will wrap if needed. The one with the most lines will determine the bottom."
}
}
class HomePageView: UIScrollView {
var homePageCarrousel: HomePageCarrousel?
var homePageSocial: HomePageSocialUp?
var homePageAboutUs: HomePageAboutUs?
var stackView = UIStackView()
func setupHomePage() {
translatesAutoresizingMaskIntoConstraints = false
homePageCarrousel = HomePageCarrousel()
homePageSocial = HomePageSocialUp()
homePageAboutUs = HomePageAboutUs()
guard let homePageSocial = homePageSocial, let homePageCarrousel = homePageCarrousel, let homePageAboutUs = homePageAboutUs else { return }
homePageSocial.setupSocialHeader()
stackView.addArrangedSubview(homePageSocial)
homePageCarrousel.setupCarrousel()
stackView.addArrangedSubview(homePageCarrousel)
homePageAboutUs.setup()
stackView.addArrangedSubview(homePageAboutUs)
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 10
setLayout()
}
func setLayout(){
// constrain stackView to scroll view's Content Layout Guide
let g = self.contentLayoutGuide
///stackview
NSLayoutConstraint.activate([
self.stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
self.stackView.topAnchor.constraint(equalTo: g.topAnchor),
self.stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
self.stackView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
// stack view width is scroll view's frame layout guide width
stackView.widthAnchor.constraint(equalTo: self.frameLayoutGuide.widthAnchor),
])
}
}

How to set content hugging and compression programmatically

I want to animate hide show when one of my view is hidden. so I'm using content hugging priority to animate that, but it failed it has a gap between view. here I show you the ui and my code
This is 3 uiview code like the picture above
scrollView.addSubview(chooseScheduleDropDown)
chooseScheduleDropDown.translatesAutoresizingMaskIntoConstraints = false
chooseScheduleDropDown.setContentCompressionResistancePriority(.required, for: .vertical)
NSLayoutConstraint.activate([
chooseScheduleDropDown.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
chooseScheduleDropDown.topAnchor.constraint(equalTo: scrollView.topAnchor),
chooseScheduleDropDown.widthAnchor.constraint(equalToConstant: 285),
chooseScheduleDropDown.heightAnchor.constraint(equalToConstant: 60)
])
scrollView.addSubview(entryView)
entryView.isHidden = true
entryView.setContentHuggingPriority(.defaultLow, for: .vertical)
entryView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
entryView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
entryView.topAnchor.constraint(equalTo: chooseScheduleDropDown.bottomAnchor, constant: topPadding),
entryView.widthAnchor.constraint(equalToConstant: 285),
entryView.heightAnchor.constraint(equalToConstant: 60)
])
scrollView.addSubview(chooseDateView)
chooseDateView.setContentHuggingPriority(.defaultLow, for: .vertical)
chooseDateView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
chooseDateView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
chooseDateView.topAnchor.constraint(equalTo: entryView.bottomAnchor, constant: topPadding),
chooseDateView.widthAnchor.constraint(equalToConstant: 285),
chooseDateView.heightAnchor.constraint(equalToConstant: 60)
])
After exchanging comments, you have a number of different tasks to work on.
But, to give you an example of one approach to showing / hiding the "middle" view and having the bottom view move up / down, here is something to try. It will look like this:
Tapping the top (red) view will hide the middle (green) view and slide the bottom (blue) view up. Tapping the top (red) view again will slide the bottom (blue) view down and show the middle (green) view.
This is done by creating two top constraints for the Bottom view. One relative to the bottom of the Top view, and the other relative to the bottom of the Middle view, with different .priority values.
The example code is fairly straight-forward, and the comments should make things clear. All done via code - no #IBOutlet or #IBAction connections - so just create a new view controller and assign its custom class to AnimTestViewController:
class DropDownView: UIView {
}
class AnimTestViewController: UIViewController {
let scrollView: UIScrollView = {
let v = UIScrollView()
return v
}()
let chooseScheduleDropDown: DropDownView = {
let v = DropDownView()
return v
}()
let entryView: DropDownView = {
let v = DropDownView()
return v
}()
let chooseDateView: DropDownView = {
let v = DropDownView()
return v
}()
var visibleConstraint: NSLayoutConstraint = NSLayoutConstraint()
var hiddenConstraint: NSLayoutConstraint = NSLayoutConstraint()
override func viewDidLoad() {
super.viewDidLoad()
[chooseScheduleDropDown, entryView, chooseDateView].forEach {
v in
v.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(v)
}
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
let g = view.safeAreaLayoutGuide
let topPadding: CGFloat = 20.0
// chooseDateView top anchor when entryView is visible
visibleConstraint = chooseDateView.topAnchor.constraint(equalTo: entryView.bottomAnchor, constant: topPadding)
// chooseDateView top anchor when entryView is hidden
hiddenConstraint = chooseDateView.topAnchor.constraint(equalTo: chooseScheduleDropDown.bottomAnchor, constant: topPadding)
// we will start with entryView visible
visibleConstraint.priority = .defaultHigh
hiddenConstraint.priority = .defaultLow
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
chooseScheduleDropDown.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
chooseScheduleDropDown.topAnchor.constraint(equalTo: scrollView.topAnchor),
chooseScheduleDropDown.widthAnchor.constraint(equalToConstant: 285),
chooseScheduleDropDown.heightAnchor.constraint(equalToConstant: 60),
entryView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
entryView.topAnchor.constraint(equalTo: chooseScheduleDropDown.bottomAnchor, constant: topPadding),
entryView.widthAnchor.constraint(equalToConstant: 285),
entryView.heightAnchor.constraint(equalToConstant: 60),
chooseDateView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
//chooseDateView.topAnchor.constraint(equalTo: entryView.bottomAnchor, constant: topPadding),
visibleConstraint,
hiddenConstraint,
chooseDateView.widthAnchor.constraint(equalToConstant: 285),
chooseDateView.heightAnchor.constraint(equalToConstant: 60),
])
//entryView.isHidden = true
chooseScheduleDropDown.backgroundColor = .red
entryView.backgroundColor = .green
chooseDateView.backgroundColor = .blue
let tap = UITapGestureRecognizer(target: self, action: #selector(toggleEntryView(_:)))
chooseScheduleDropDown.addGestureRecognizer(tap)
}
#objc func toggleEntryView(_ sender: UITapGestureRecognizer) -> Void {
print("tapped")
// if entryView IS hidden we want to
// un-hide entryView
// animate alpha to 1.0
// animate chooseDateView down
// if entryView is NOT hidden we want to
// animate alpha to 0.0
// animate chooseDateView up
// hide entryView when animation is finished
let animSpeed = 0.5
if entryView.isHidden {
entryView.isHidden = false
hiddenConstraint.priority = .defaultLow
visibleConstraint.priority = .defaultHigh
UIView.animate(withDuration: animSpeed, animations: {
self.entryView.alpha = 1.0
self.view.layoutIfNeeded()
}, completion: { _ in
})
} else {
visibleConstraint.priority = .defaultLow
hiddenConstraint.priority = .defaultHigh
UIView.animate(withDuration: animSpeed, animations: {
self.entryView.alpha = 0.0
self.view.layoutIfNeeded()
}, completion: { _ in
self.entryView.isHidden = true
})
}
}
}
Do
// declare an instance property
var hCon:NSLayoutConstraint!
// get only the height constraint out of the activate block
hCon = entryView.heightAnchor.constraint(equalToConstant: 60)
hCon.isActive = true
and play with
hCon.constant = 300 / 0
view.layoutIfNeeded()

Resources